【Unity2D游戏开发入门第一卷】✨Unity入门总结Sunnyland示例(上卷)

您所在的位置:网站首页 unity 导入素材 【Unity2D游戏开发入门第一卷】✨Unity入门总结Sunnyland示例(上卷)

【Unity2D游戏开发入门第一卷】✨Unity入门总结Sunnyland示例(上卷)

2024-07-10 07:41:54| 来源: 网络整理| 查看: 265

✨✨目录 一、入门卷 二、杂项卷 三、最后

一、入门卷

回到顶部

前言

准备资源

Tilemap 地图布置,刚体组件

角色移动跳跃脚本,以及刚体,碰撞器等组件添加(包含射线检测,解决手感问题)

角色添加动画逻辑,以及动画组件(包含动画状态机设置)

相机跟随 Player 移动(简单代码实现)

游戏场景切换

[入门卷] 0. 前言

回到顶部

本卷以 Sunnyland 素材为例,简单总结了一下 Unity 在 2D 游戏制作方面的基本使用。

[入门卷] 1. 准备资源

回到顶部

新建2D项目 FirstGameSunnyland

A. 导入资源 1. 添加资源

菜单栏 Window -> Asset Store -> Search online 搜索 Sunnyland -> 添加至我的资源

2. 导入素材

-> 在 Unity 中打开 或者 在Unity编辑器中 菜单栏 Window -> Package Manager

-> 左上角 Packages 选择 My Assets -> 选中 Sunny Land Download -> Import

-> All, Import

B. 整理文件夹

我习惯在 Asset 文件夹中新建 _Game 文件夹,前缀 _ 按对应字典排序在前面,会出现在比较靠前的位置,这个不做强制要求。

在 _Game 中暂时新建 Animations,Prefabs,Scripts,Maps,Scenes,Materials 6个文件夹。

我还把外面的两个文件夹拖了进来

C. 资源格式设置

在游戏中一般都会使用相同的 Pixels Per Units,默认是 100,这里我使用 16,可以根据需求更改,不同资源格式可以不一样,在使用资源时注意更改即可。

D. 编辑器视图布局

我之前使用的是在默认布局基础上做了一点更改,可以在右上角,Layout 点击 Default 设置。

可以右键下方 Project 或 Console,Add Tab -> Project,我喜欢用两个 Project

下面再加几个常用的窗口:

Window -> 2D -> Tile Palette (可以拖拽到自己喜欢的位置)

Window -> Animation -> Animation(动画制作)

Window -> Animation -> Animator(动画状态机控制)

注意:没必要跟我一模一样,这只是我目前的习惯,你完全可以按照你的喜好来

[入门卷] 2. Tilemap 地图布置,Tilemap 碰撞体组件

回到顶部

A. Tilemap 地图布置

xxx/Sunnyland/artwork/Environment 下有一些资源。

(1). 背景

可以拖一张背景图片 back 到 Scene 场景中,重命名为 Background,在 Inspector 中,Transform 组件右上角 三点左击 Reset 可以重置 Transform 中的属性。这里将 Scale 设置为 (3,3,3),复制两份(可以手动复制,或者 Ctrl+D),按住 V 会出现锚点,拖动这个锚点往想去的那个地方拖拽(有类似吸附效果),将 3个背景稍微整齐排列。

(2). Tileset 瓦片集

资源自带的一个 tileset,已经切割好了,不过这里为了介绍基本使用,我们可以再切割一次

(3). 素材切割

点击 tileset,在 Inspector 中,将 Sprite Mode 改为 Multiple,点击 Sprite Editor 进行切割。

Slice -> Type 选中 Grid By Cell Size -> Pixel Size 改为 x16 y16(因为我将素材都改为了 16像素每单位) -> 最后点击 Slice 切割 -> Apply

(4). Sprite 图层 Layer

这里简单介绍一下图层的概念,Unity 中的图层渲染顺序是从上开始往下渲染的,也就是说越下面的图层越在上面,或者你也可以更改 Orider in Layer,数字越大越在上面

选择 Tilemap_BaseMap 这里在 Inspector -> xxx Renderer(如 Tilemap Renderer) 中的 Sorting Layer 中从上到下添加了 Background,Environment,Foreground

在 Hierarchy 窗口中右键创建空物体 Create Empty 重命名为 Background,来管理其他 3个背景,其他按格式 bg 重命名

选中 Background 在 Inspector 窗口中添加组件 Sprite Renderer,设置 Sorting Layer 为 Background

(5). 地图布置 这里好像自带了一个 Main Palette,暂时不用管他,在 Tile Palette 窗口中 Create New Palette,我将其命名为 BaseMap,放入 _Game/Maps/BaseMap 中,将切割好的 tileset 素材拖入 Tile Palette 窗口对应的 Palette 中。

在 Hierarchy 窗口中,右键 2D Object -> Tilemap -> Rectangular,重命名 Tilemap 为 Tilemap_BaseMap。

接下来可以使用 Tile Palette 窗口中对应的 Palette 资源在刚刚创建的 Grid 中的 Tilemap 上绘制了,面板上面有很多工具,如笔刷,橡皮。注意:Tile Palette 窗口中 Active Tilemap 要选择需要绘制的 Tilemap。

你还可以选择 Edit 选项去编辑 Tile Palette 如果你去试试,会很简单

这里布置地图如下 不过你完全可以按你的喜好来,不要让文章限制你的想法,我这里布置的比较随意,因为可能用不到这么大的地图,本卷主要以介绍基本使用和逻辑为主

B. Tilemap 碰撞体组件

为 Tilemap_BaseMap 添加 [复合] 碰撞体组件。

点击 Tilemap_BaseMap,在 Inspector 窗口下面,Add Component,搜索 Tilemap Collider 2D 并添加,这里勾选下面的 Used By Composite 选项(防止碰撞体之间的卡住现象,可以自行试一下,例如,有时角色冻结 Z 轴,移动会卡住,不冻结会是绕 Z轴旋转的现象),若勾选了此选项,还需要添加 Composite Collider 2D 组件,但此时会自动添加 Rigidbody 2D,默认会有重力,这不是我们想要的,简单的可以将 Rigidbody 2D 中的 Body Type 改为 Static,或将重力设为 0。

将鼠标放在 Hierarchy 物体对象上左边会有小眼睛,点击可以隐藏该组件 你现在可以试试隐藏 Background 然后选择 Tilemap_BaseMap 查看刚刚设置的 复合 碰撞体组件了

[入门卷] 3. 角色移动跳跃脚本,以及刚体,碰撞器等组件添加(包含射线检测,解决手感问题)

回到顶部

在 .../Sunnyland/artwork/Sprites/player/idle 中找到 Player 的 “闲置状态” 素材,注意 Pixels Per Unit 的设置 这里统一 16。将 第一张素材图片拖入 Scene 中,或者在 hierarchy 中右键创建 2D Object -> Sprites -> Square 或者随便选一个,这里选的是 Square(这个只是设置 Renderer 中的 Sprite,后面要改,所以随便选择),将 Sprite 设置为第一张素材图片,可以拖拽,将 Sorting Layer 设置为 Foreground。

注意命名该对象为 Player

A. 刚体,碰撞器组件

添加 Rigidbody 2D【重力设置为 3】,Capsule Collider 2D

在 Rigidbody 2D 中,设置 Collision Detection -> Continuous(让检测更频繁),Interpolate -> Interpolate(落地会有一点凹陷,然后恢复,模拟更真实的效果)。在 Constraints 中,勾选 Freeze Rotation,防止 Z 轴翻滚。

在 Capsule Collider 2D 中,点击 Edit Collider 可以设置 碰撞体形状,设置成合适的大小。(如果看不清,可以先隐藏 背景 Background)

给 Player 添加材质。在 .../_Game/Materials/ 中右键 Create -> 2D -> Physics Material 2D,命名为 M_ZeroFriction,我一般喜欢将材质命名为 M 开头,将 Friction 设为 0。拖到 Capsule Collider 2D 的 Material 上。

B. 按键设置

Edit -> Project Settings -> Input Manager -> Axes 展开

这里会用到里面自带 Horizontal 和 Jump

再添加一个 Crouch 按键用来触发 下蹲。随便用鼠标右击一个按键例如 Jump -> Duplicate Array Element,复制一份,再更改 Name 为 Crouch,Positive Button 为 s,Alt Positive Button,是替代按键,暂时不用设置。

C. 为 Player 添加脚本组件(暂时不包括动画) (1). 配置编辑器

✨ 配置 VsCode 参考链接

如果是 Visual Studio 例如(Visual Studio 2019)直接使用,不用配置,这里使用的是 VS 2019

Edit -> Preferences -> Analysis > External Tools -> External Script Editor

(2). 示例代码

在 Project 中右键 Create -> C# Script(拖到 Player 上添加组件),或者 直接在 Inspector 中添加。这里选择前者,后者会默认创建在 Assets 目录,还要手动移动到 Scripts 文件夹中,所以就直接右键在 指定文件夹中创建了。(双击组件属性 Script 或 脚本文件进入 Vscode 编写脚本)

PlayerController.cs 移动示例代码如下(包括,水平移动,转向,跳跃,下蹲,射线检测)

using System.Collections; using System.Collections.Generic; using UnityEngine; public class PlayerController : MonoBehaviour { // --- Private ---------------------------------- private Rigidbody2D m_Rb; // Player 刚体组件 private CapsuleCollider2D m_CapsuleCollider2D; // 胶囊碰撞体组件 // 检测相关 [Header("【只能看不能改】")] [SerializeField] private bool m_BTurnDirection = false; // Player 是否转向 [SerializeField] private bool m_BDoJump = false; // 是否执行跳跃相关 [SerializeField] private bool m_BPressedCrouch = false; // 是否按下 下蹲键 [SerializeField] private bool m_BOnGround = false; // 是否在地面 [SerializeField] private int m_CurrentAllowAirJumpCount = 0;// 当前 允许跳空中跃次数 [SerializeField] private Vector2 m_CapsuleCollider2DSize; // 保存碰撞体 初始大小 [SerializeField] private Vector2 m_CapsuleCollider2DOffset; // 保存碰撞体 初始偏移 [SerializeField] private bool m_BTopHasWall = false; // 上方是否有墙 // --- Public ----------------------------------- [Header("【移动参数】")] public float m_HorizontalSpeedFactorPerSecond = 300.0f; // 水平移动速度 public float m_JumpSpeedPerSecond = 520.0f; // 跳跃速度 public int m_AllowAirJumpCount = 1; // 允许跳空中跃次数 [Header("【检测相关】")] public LayerMask m_LMGround; // 地面图层蒙版 public float m_CheckGround_Left_XOffset = -0.4f; // 地面检测 射线 x 左偏移 public float m_CheckGround_Right_XOffset = 0.2f; // 地面检测 射线 x 右偏移 public float m_CheckGround_YOffset = -0.95f; // 地面检测 射线 y 偏移 public float m_CheckGround_Distance = 0.1f; // 地面检测 射线发射距离 public float m_CheckTopHasWall_Distance = 0.4f; // 检测上方是否右墙 射线 距离 public float m_CheckTopHasWall_Left_XOffset = -0.4f; // 地面检测 射线 x 左偏移 public float m_CheckTopHasWall_Right_XOffset = 0.35f; // 地面检测 射线 x 右偏移 // Start is called before the first frame update private void Start() { // 获取组件 m_Rb = GetComponent(); m_CapsuleCollider2D = GetComponent(); // 设置参数 m_CurrentAllowAirJumpCount = m_AllowAirJumpCount; m_CapsuleCollider2DSize = m_CapsuleCollider2D.size; m_CapsuleCollider2DOffset = m_CapsuleCollider2D.offset; } private void FixedUpdate() { Move(); } // Update is called once per frame private void Update() { Check(); } // --- public ------------------------------------------- /// /// @breif 移动 /// public void Move() { HorizontalMove(); // 水平移动 Jump(); // 跳跃 Crouch(); // 下蹲 } /// /// @brief 检测部分 /// public void Check() { CheckInput(); // 输入检测 CheckOnGround(); // 检测是否在地面 CheckTopHasWall(); // 检测头上是否有墙壁 } // --- private ------------------------------------------ /// /// @brief 输入检测 /// private void CheckInput() { // 检测跳跃键【GetButtonDown 这个函数,一直按下也只会算一次,需要松开再按才算下一次】 //Debug.LogWarning(Input.GetButtonDown("Jump")); if (Input.GetButtonDown("Jump") && m_CurrentAllowAirJumpCount > 0) { // 如果可以跳跃,执行跳跃相关 m_BDoJump = true; } // 检测下蹲键 m_BPressedCrouch = Input.GetButton("Crouch"); } /// /// @brief 检测上方是否有墙壁 /// private void CheckTopHasWall() { float yOffset = -0.2f; // 检测上方是否有墙壁的偏移量 // 射线起点 Vector2 leftStart2 = new Vector2(transform.position.x + m_CheckTopHasWall_Left_XOffset, transform.position.y + yOffset); Vector2 rightStart2 = new Vector2(transform.position.x + m_CheckTopHasWall_Right_XOffset, transform.position.y + yOffset); // 射线方向 Vector2 direction2 = Vector2.up; #if DEBUG // 调试用变量 Vector3 leftStart3 = new Vector3(transform.position.x + m_CheckTopHasWall_Left_XOffset, transform.position.y + yOffset, 0.0f); Vector3 rightStart3 = new Vector3(transform.position.x + m_CheckTopHasWall_Right_XOffset, transform.position.y + yOffset, 0.0f); Vector3 direction3 = Vector3.up; #endif // 射线持续时间(Debug) float durationTime = 0.0f; RaycastHit2D leftHitResult = Physics2D.Raycast(leftStart2, direction2, m_CheckTopHasWall_Distance, m_LMGround); RaycastHit2D rightHitResult = Physics2D.Raycast(rightStart2, direction2, m_CheckTopHasWall_Distance, m_LMGround); if (leftHitResult || rightHitResult) { m_BTopHasWall = true; // Debug #if DEBUG Debug.DrawLine(leftStart3, leftStart3 + direction3 * m_CheckTopHasWall_Distance, Color.green, durationTime); // 绿 Debug.DrawLine(rightStart3, rightStart3 + direction3 * m_CheckTopHasWall_Distance, Color.green, durationTime); // 绿 #endif } else { m_BTopHasWall = false; // Debug #if DEBUG Debug.DrawLine(leftStart3, leftStart3 + direction3 * m_CheckTopHasWall_Distance, Color.red, durationTime); // 红 Debug.DrawLine(rightStart3, rightStart3 + direction3 * m_CheckTopHasWall_Distance, Color.red, durationTime); // 红 #endif } } /// /// @brief 检测是否在地面上 /// private void CheckOnGround() { // 射线起点 Vector2 leftStart2 = new Vector2(transform.position.x + m_CheckGround_Left_XOffset, transform.position.y + m_CheckGround_YOffset); Vector2 rightStart2 = new Vector2(transform.position.x + m_CheckGround_Right_XOffset, transform.position.y + m_CheckGround_YOffset); Vector2 leftTurnDirectionStart2 = new Vector2(leftStart2.x + 0.18f, leftStart2.y); Vector2 rightTurnDirectionStart2 = new Vector2(rightStart2.x + 0.22f, rightStart2.y); #if DEBUG Vector3 leftStart3 = new Vector3(leftStart2.x, leftStart2.y, 0.0f); Vector3 rightStart3 = new Vector3(rightStart2.x, rightStart2.y, 0.0f); Vector3 leftTurnDirectionStart3 = new Vector3(leftTurnDirectionStart2.x, leftTurnDirectionStart2.y, 0.0f); Vector3 rightTurnDirectionStart3 = new Vector3(rightTurnDirectionStart2.x, rightTurnDirectionStart2.y, 0.0f); #endif // 射线方向 Vector2 direction2 = Vector2.down; Vector3 direction3 = Vector3.down; // 射线持续时间(Debug) float durationTime = 0.0f; // 射线结果,该结构体也有重写 bool operator,所以可以直接用来判断 RaycastHit2D leftHitResult; RaycastHit2D rightHitResult; if (m_BTurnDirection) // 如果转向了 { leftHitResult = Physics2D.Raycast(leftTurnDirectionStart2, direction2, m_CheckGround_Distance, m_LMGround); rightHitResult = Physics2D.Raycast(rightTurnDirectionStart2, direction2, m_CheckGround_Distance, m_LMGround); } else // 如果没转向 { leftHitResult = Physics2D.Raycast(leftStart2, direction2, m_CheckGround_Distance, m_LMGround); rightHitResult = Physics2D.Raycast(rightStart2, direction2, m_CheckGround_Distance, m_LMGround); } if (leftHitResult.collider || rightHitResult.collider) { // 在地面上 m_BOnGround = true; // 重置空中跳跃次数 m_CurrentAllowAirJumpCount = m_AllowAirJumpCount; // 调试,击中为绿色 #if DEBUG if (m_BTurnDirection) { Debug.DrawLine(leftTurnDirectionStart3, leftTurnDirectionStart3 + direction3 * m_CheckGround_Distance, Color.green, durationTime); Debug.DrawLine(rightTurnDirectionStart3, rightTurnDirectionStart3 + direction3 * m_CheckGround_Distance, Color.green, durationTime); } else { Debug.DrawLine(leftStart3, leftStart3 + direction3 * m_CheckGround_Distance, Color.green, durationTime); Debug.DrawLine(rightStart3, rightStart3 + direction3 * m_CheckGround_Distance, Color.green, durationTime); } #endif } else { // 不在地面上 m_BOnGround = false; // 调试 #if DEBUG if (m_BTurnDirection) { Debug.DrawLine(leftTurnDirectionStart3, leftTurnDirectionStart3 + direction3 * m_CheckGround_Distance, Color.red, durationTime); Debug.DrawLine(rightTurnDirectionStart3, rightTurnDirectionStart3 + direction3 * m_CheckGround_Distance, Color.red, durationTime); } else { Debug.DrawLine(leftStart3, leftStart3 + direction3 * m_CheckGround_Distance, Color.red, durationTime); Debug.DrawLine(rightStart3, rightStart3 + direction3 * m_CheckGround_Distance, Color.red, durationTime); } #endif } } /// /// @brief 水平移动 /// private void HorizontalMove() { // 获取水平输入 [-1, 1],松开按键时为 0 float horizontalValue = Input.GetAxis("Horizontal"); m_Rb.velocity = new Vector2(horizontalValue * m_HorizontalSpeedFactorPerSecond * Time.fixedDeltaTime, m_Rb.velocity.y); // Player 转向 if (horizontalValue < 0.0f) // 默认是右边 { m_BTurnDirection = true; transform.rotation = new Quaternion(0.0f, 180.0f, 0.0f, 1.0f); } else if (horizontalValue > 0.0f) { m_BTurnDirection = false; transform.rotation = new Quaternion(0.0f, 0.0f, 0.0f, 1.0f); } } /// /// @brief 跳跃 /// private void Jump() { if (m_BOnGround && m_BDoJump) { m_BDoJump = false; m_Rb.velocity = new Vector2(m_Rb.velocity.x, m_JumpSpeedPerSecond * Time.fixedDeltaTime); } if (!m_BOnGround && m_CurrentAllowAirJumpCount > 0 && m_BDoJump) { // 空中跳跃 m_BDoJump = false; --m_CurrentAllowAirJumpCount; m_Rb.velocity = new Vector2(m_Rb.velocity.x, m_JumpSpeedPerSecond * Time.fixedDeltaTime); } } /// /// @brief 下蹲 /// private void Crouch() { if (m_BPressedCrouch) { m_CapsuleCollider2D.size = new Vector2(m_CapsuleCollider2D.size.x, 0.6f); m_CapsuleCollider2D.offset = new Vector2(m_CapsuleCollider2D.offset.x, -0.545f); } else if (!m_BTopHasWall) // 上方如果检测到有墙不能起来 { m_CapsuleCollider2D.size = m_CapsuleCollider2DSize; m_CapsuleCollider2D.offset = m_CapsuleCollider2DOffset; } } }

添加一个 Layer 命名为 Ground,不要忘记将 Tilemap_BaseMap 的图层 Layer 设置为 Ground

编辑器参数设置

如果脚本更改,一般需要刷新组件才能更新编辑器中的数值显示

这里应该只需要修改图层为 Ground

(3). 特别注意

常见的是跳跃手感问题,代码逻辑不当会让玩家感觉跳跃按键不灵。

简单说一下这里使用的一些手段,其他代码应该挺简单的,结合注释应该很好阅读

1.** 将检测部分放入 Update 中,将与刚体运动相关的放入 FixedUpdate 中。**

代码逻辑,参考如下(从上方摘取)【这里指出关键代码让大家体会】

检测部分放入 Update 中

[SerializeField] private bool m_BDoJump = false; // 是否执行跳跃相关 private void CheckInput() { // 检测跳跃键【GetButtonDown 这个函数,一直按下也只会算一次,需要松开再按才算下一次】 //Debug.LogWarning(Input.GetButtonDown("Jump")); if (Input.GetButtonDown("Jump") && m_CurrentAllowAirJumpCount > 0) { // 如果可以跳跃,执行跳跃相关 m_BDoJump = true; } // 检测下蹲键 m_BPressedCrouch = Input.GetButton("Crouch"); }

与刚体运动相关的放入 FixedUpdate 中

public float m_JumpSpeedPerSecond = 520.0f; // 跳跃速度 private void Jump() { if (m_BOnGround && m_BDoJump) { m_BDoJump = false; m_Rb.velocity = new Vector2(m_Rb.velocity.x, m_JumpSpeedPerSecond * Time.fixedDeltaTime); } if (!m_BOnGround && m_CurrentAllowAirJumpCount > 0 && m_BDoJump) { // 空中跳跃 m_BDoJump = false; --m_CurrentAllowAirJumpCount; m_Rb.velocity = new Vector2(m_Rb.velocity.x, m_JumpSpeedPerSecond * Time.fixedDeltaTime); } } (4). Unity 函数生命周期参考

[入门卷] 4. 角色添加动画逻辑,以及动画组件(包含动画状态机设置)

回到顶部

为 Player 添加 Animator 组件(或者先不添加),然后在 .../_Game/Animations/Player 中 右键 Create -> Animator Controller,这里命名为 AC_Player 并拖入 Hierarchy 面板上的 Player 对象中 或者 拖入 Player 的 Inspector -> Animator -> Controller 上。

在此目录下创建动画,在 Animation 窗口中,点击右下框的 Create,创建动画,这里命名为 Idling.anim,创建一次后即可在 左边选择动画列表中(例如这边叫做 Player,左键点一下就会弹出下拉菜单),下方点击 Create New Clip 创建动画。暂时创建 Idling(刚刚创建了,不用创建了),Running,Jumping,Falling,Crouching 5个动画片段。

注意:将素材设置为统一的格式,然后这里 Pixels Per Unit 设置的是 16 !

把相应的动画素材全选拖入 Animation 右边时间轴窗口。点击右边第二行三点,Show Sample Rate (我比较喜欢用),这个可以调节频率,按自己喜好调节 (我这里Idling,Running,Crouching 分别调节的是 10, 10, 7)Jumping 和 Falling 只有一张,我将 Samples 设置了 1,不知道是否能改善性能呢?,红色录制按钮右边还有播放按钮,非常方便。考虑本卷主要介绍基本使用,所有不弄太复杂,之后可能会单独出模块介绍。

设置 Animator。(也可以不使用 Unity 编辑器中的动画状态机,可以在代码操作,但是这次使用的是动画状态机)

右键单个动画片段,可以 Make Transition 然后关联(连线)动画片段。

动画控制器视图参考

✨ A. 动画控制器属性设置如下

【这里可以耐心配置一下,截图会占太多篇幅,所以可能需要大家稍微花点时间过一下,挺快的,我都打出来了,哈哈】

(1). 参数设置

Inspector 面板中,右下角有 Conditions List 条件列表,有 +、- 可以操作 CRUD 列表

Idling -> Running Conditions: HorizontalSpeedPerSecond > 0.1 Idling Jumping Conditions: BJumping = true;

Jumping -> Falling Conditions: BFalling = true; Falling -> Jumping Conditions: BJumping = true, BFalling = false;

Falling -> Idling Conditions: BFalling = false, BIdling = true; Idling -> Falling Conditions: BFalling = true;

Idling -> Crouching Conditions: BCrouching = true; Crouching -> Idling Conditions: BCrouching = false, BIdling = true;

Running -> Crouching Conditions: BCrouching = true; Crouching -> Running Conditions: BCrouching = false, HorizontalSpeedPerSec > 0.1;

Crouching -> Jumping Conditions: BCrouching = false, BJumping = true; Falling -> Crouching Conditions: BCrouching = true, BFalling = true;

Idling -> Jumping Conditions: BJumping = true; Jumping -> Idling Conditions: BIdling = true, BFalling = false;

(2). 其他设置

把所有动画 Inspector 中,Has Exit Time 取消勾选,Settings -> Transition Duration(s) = 0。

但是 Idling -> Flling,Idling -> Jumping,Transition Duration(s) = 0.1,防止警告。

B. 添加脚本,加入动画控制逻辑,如下 添加组件 private Animator _Animator; // Animator 在按键检测注释下面添加 // 检测动画状态 [SerializeField] private bool m_BAnimIdling = true; // 闲置状态 [SerializeField] private bool m_BAnimRunning = false; // 跑动状态 [SerializeField] private bool m_BAnimJumping = false; // 跳跃状态 [SerializeField] private bool m_BAnimFalling = false; // 下落状态 [SerializeField] private bool m_BAnimCrouching = false; // 下蹲状态 Start() 获取组件下面添加 m_Animator = GetComponent(); 添加方法 /// /// @brief 动画控制 /// public void AnimatorControl() { m_Animator.SetFloat("HorizontalSpeedPerSecond", Mathf.Abs(m_Rb.velocity.x)); m_Animator.SetBool("BIdling", m_BAnimIdling); m_Animator.SetBool("BJumping", m_BAnimJumping); m_Animator.SetBool("BFalling", m_BAnimFalling); m_Animator.SetBool("BCrouching", m_BAnimCrouching); } /// /// 检测状态 /// private void CheckState() { // 跑动 m_BAnimRunning = (Mathf.Abs(m_Rb.velocity.x) > 0.1f && !m_BAnimCrouching && !m_BAnimJumping && !m_BAnimFalling); // 下落 m_BAnimFalling = m_Rb.velocity.y < -0.1f; // 跳跃 if ((m_BOnGround && m_BDoJump) || (!m_BOnGround && m_AllowAirJumpCount > 0 && m_BDoJump)) { m_BAnimJumping = true; } else if (m_BAnimFalling) { m_BAnimJumping = false; } // 下蹲 m_BAnimCrouching = (m_BOnGround && ((m_BPressedCrouch && (!m_BAnimJumping || !m_BAnimFalling)) || m_BTopHasWall)); // 闲置 m_BAnimIdling = (m_BOnGround && !m_BAnimCrouching && !m_BAnimFalling); }

AnimatorControl() 添加到 Update() 中,CheckState() 添加到 Check() 中

C. 添加动画逻辑后的完整代码 using System.Collections; using System.Collections.Generic; using UnityEngine; public class PlayerController : MonoBehaviour { // --- Private ---------------------------------- private Rigidbody2D m_Rb; // Player 刚体组件 private CapsuleCollider2D m_CapsuleCollider2D; // 胶囊碰撞体组件 private Animator m_Animator; // 动画控制器 /** 检测相关 */ [Header("【只能看不能改】")] [SerializeField] private bool m_BTurnDirection = false; // Player 是否转向 [SerializeField] private bool m_BDoJump = false; // 是否执行跳跃相关 [SerializeField] private bool m_BPressedCrouch = false; // 是否按下 下蹲键 [SerializeField] private bool m_BOnGround = false; // 是否在地面 [SerializeField] private int m_CurrentAllowAirJumpCount = 0;// 当前 允许跳空中跃次数 [SerializeField] private Vector2 m_CapsuleCollider2DSize; // 保存碰撞体 初始大小 [SerializeField] private Vector2 m_CapsuleCollider2DOffset; // 保存碰撞体 初始偏移 [SerializeField] private bool m_BTopHasWall = false; // 上方是否有墙 // 检测动画状态 [SerializeField] private bool m_BAnimIdling = true; // 闲置状态 [SerializeField] private bool m_BAnimRunning = false; // 跑动状态 [SerializeField] private bool m_BAnimJumping = false; // 跳跃状态 [SerializeField] private bool m_BAnimFalling = false; // 下落状态 [SerializeField] private bool m_BAnimCrouching = false; // 下蹲状态 // --- Public ----------------------------------- [Header("【移动参数】")] public float m_HorizontalSpeedFactorPerSecond = 300.0f; // 水平移动速度 public float m_JumpSpeedPerSecond = 520.0f; // 跳跃速度 public int m_AllowAirJumpCount = 1; // 允许跳空中跃次数 [Header("【检测相关】")] public LayerMask m_LMGround; // 地面图层蒙版 public float m_CheckGround_Left_XOffset = -0.4f; // 地面检测 射线 x 左偏移 public float m_CheckGround_Right_XOffset = 0.2f; // 地面检测 射线 x 右偏移 public float m_CheckGround_YOffset = -0.95f; // 地面检测 射线 y 偏移 public float m_CheckGround_Distance = 0.1f; // 地面检测 射线发射距离 public float m_CheckTopHasWall_Distance = 0.4f; // 检测上方是否右墙 射线 距离 public float m_CheckTopHasWall_Left_XOffset = -0.4f; // 地面检测 射线 x 左偏移 public float m_CheckTopHasWall_Right_XOffset = 0.35f; // 地面检测 射线 x 右偏移 // Start is called before the first frame update private void Start() { // 获取组件 m_Rb = GetComponent(); m_CapsuleCollider2D = GetComponent(); m_Animator = GetComponent(); // 设置参数 m_CurrentAllowAirJumpCount = m_AllowAirJumpCount; m_CapsuleCollider2DSize = m_CapsuleCollider2D.size; m_CapsuleCollider2DOffset = m_CapsuleCollider2D.offset; } private void FixedUpdate() { Move(); } // Update is called once per frame private void Update() { Check(); // 检测 AnimatorControl(); // 动画控制 } // --- public ------------------------------------------- /// /// @breif 移动 /// public void Move() { HorizontalMove(); // 水平移动 Jump(); // 跳跃 Crouch(); // 下蹲 } /// /// @brief 检测部分 /// public void Check() { CheckInput(); // 输入检测 CheckOnGround(); // 检测是否在地面 CheckTopHasWall(); // 检测头上是否有墙壁 CheckState(); } /// /// @brief 动画控制 /// public void AnimatorControl() { m_Animator.SetFloat("HorizontalSpeedPerSecond", Mathf.Abs(m_Rb.velocity.x)); m_Animator.SetBool("BIdling", m_BAnimIdling); m_Animator.SetBool("BJumping", m_BAnimJumping); m_Animator.SetBool("BFalling", m_BAnimFalling); m_Animator.SetBool("BCrouching", m_BAnimCrouching); } // --- private ------------------------------------------ /// /// 检测状态 /// private void CheckState() { // 跑动 m_BAnimRunning = (Mathf.Abs(m_Rb.velocity.x) > 0.1f && !m_BAnimCrouching && !m_BAnimJumping && !m_BAnimFalling); // 下落 m_BAnimFalling = m_Rb.velocity.y < -0.1f; // 跳跃 if ((m_BOnGround && m_BDoJump) || (!m_BOnGround && m_AllowAirJumpCount > 0 && m_BDoJump)) { m_BAnimJumping = true; } else if (m_BAnimFalling) { m_BAnimJumping = false; } // 下蹲 m_BAnimCrouching = (m_BOnGround && ((m_BPressedCrouch && (!m_BAnimJumping || !m_BAnimFalling)) || m_BTopHasWall)); // 闲置 m_BAnimIdling = (m_BOnGround && !m_BAnimCrouching && !m_BAnimFalling); } /// /// @brief 输入检测 /// private void CheckInput() { // 检测跳跃键【GetButtonDown 这个函数,一直按下也只会算一次,需要松开再按才算下一次】 //Debug.LogWarning(Input.GetButtonDown("Jump")); if (Input.GetButtonDown("Jump") && m_CurrentAllowAirJumpCount > 0) { // 如果可以跳跃,执行跳跃相关 m_BDoJump = true; } // 检测下蹲键 m_BPressedCrouch = Input.GetButton("Crouch"); } /// /// @brief 检测上方是否有墙壁 /// private void CheckTopHasWall() { float yOffset = -0.2f; // 检测上方是否有墙壁的偏移量 // 射线起点 Vector2 leftStart2 = new Vector2(transform.position.x + m_CheckTopHasWall_Left_XOffset, transform.position.y + yOffset); Vector2 rightStart2 = new Vector2(transform.position.x + m_CheckTopHasWall_Right_XOffset, transform.position.y + yOffset); // 射线方向 Vector2 direction2 = Vector2.up; #if DEBUG // 调试用变量 Vector3 leftStart3 = new Vector3(transform.position.x + m_CheckTopHasWall_Left_XOffset, transform.position.y + yOffset, 0.0f); Vector3 rightStart3 = new Vector3(transform.position.x + m_CheckTopHasWall_Right_XOffset, transform.position.y + yOffset, 0.0f); Vector3 direction3 = Vector3.up; #endif // 射线持续时间(Debug) float durationTime = 0.0f; RaycastHit2D leftHitResult = Physics2D.Raycast(leftStart2, direction2, m_CheckTopHasWall_Distance, m_LMGround); RaycastHit2D rightHitResult = Physics2D.Raycast(rightStart2, direction2, m_CheckTopHasWall_Distance, m_LMGround); if (leftHitResult || rightHitResult) { m_BTopHasWall = true; // Debug #if DEBUG Debug.DrawLine(leftStart3, leftStart3 + direction3 * m_CheckTopHasWall_Distance, Color.green, durationTime); // 绿 Debug.DrawLine(rightStart3, rightStart3 + direction3 * m_CheckTopHasWall_Distance, Color.green, durationTime); // 绿 #endif } else { m_BTopHasWall = false; // Debug #if DEBUG Debug.DrawLine(leftStart3, leftStart3 + direction3 * m_CheckTopHasWall_Distance, Color.red, durationTime); // 红 Debug.DrawLine(rightStart3, rightStart3 + direction3 * m_CheckTopHasWall_Distance, Color.red, durationTime); // 红 #endif } } /// /// @brief 检测是否在地面上 /// private void CheckOnGround() { // 射线起点 Vector2 leftStart2 = new Vector2(transform.position.x + m_CheckGround_Left_XOffset, transform.position.y + m_CheckGround_YOffset); Vector2 rightStart2 = new Vector2(transform.position.x + m_CheckGround_Right_XOffset, transform.position.y + m_CheckGround_YOffset); Vector2 leftTurnDirectionStart2 = new Vector2(leftStart2.x + 0.18f, leftStart2.y); Vector2 rightTurnDirectionStart2 = new Vector2(rightStart2.x + 0.22f, rightStart2.y); #if DEBUG Vector3 leftStart3 = new Vector3(leftStart2.x, leftStart2.y, 0.0f); Vector3 rightStart3 = new Vector3(rightStart2.x, rightStart2.y, 0.0f); Vector3 leftTurnDirectionStart3 = new Vector3(leftTurnDirectionStart2.x, leftTurnDirectionStart2.y, 0.0f); Vector3 rightTurnDirectionStart3 = new Vector3(rightTurnDirectionStart2.x, rightTurnDirectionStart2.y, 0.0f); #endif // 射线方向 Vector2 direction2 = Vector2.down; Vector3 direction3 = Vector3.down; // 射线持续时间(Debug) float durationTime = 0.0f; // 射线结果,该结构体也有重写 bool operator,所以可以直接用来判断 RaycastHit2D leftHitResult; RaycastHit2D rightHitResult; if (m_BTurnDirection) // 如果转向了 { leftHitResult = Physics2D.Raycast(leftTurnDirectionStart2, direction2, m_CheckGround_Distance, m_LMGround); rightHitResult = Physics2D.Raycast(rightTurnDirectionStart2, direction2, m_CheckGround_Distance, m_LMGround); } else // 如果没转向 { leftHitResult = Physics2D.Raycast(leftStart2, direction2, m_CheckGround_Distance, m_LMGround); rightHitResult = Physics2D.Raycast(rightStart2, direction2, m_CheckGround_Distance, m_LMGround); } if (leftHitResult.collider || rightHitResult.collider) { // 在地面上 m_BOnGround = true; // 重置空中跳跃次数 m_CurrentAllowAirJumpCount = m_AllowAirJumpCount; // 调试,击中为绿色 #if DEBUG if (m_BTurnDirection) { Debug.DrawLine(leftTurnDirectionStart3, leftTurnDirectionStart3 + direction3 * m_CheckGround_Distance, Color.green, durationTime); Debug.DrawLine(rightTurnDirectionStart3, rightTurnDirectionStart3 + direction3 * m_CheckGround_Distance, Color.green, durationTime); } else { Debug.DrawLine(leftStart3, leftStart3 + direction3 * m_CheckGround_Distance, Color.green, durationTime); Debug.DrawLine(rightStart3, rightStart3 + direction3 * m_CheckGround_Distance, Color.green, durationTime); } #endif } else { // 不在地面上 m_BOnGround = false; // 调试 #if DEBUG if (m_BTurnDirection) { Debug.DrawLine(leftTurnDirectionStart3, leftTurnDirectionStart3 + direction3 * m_CheckGround_Distance, Color.red, durationTime); Debug.DrawLine(rightTurnDirectionStart3, rightTurnDirectionStart3 + direction3 * m_CheckGround_Distance, Color.red, durationTime); } else { Debug.DrawLine(leftStart3, leftStart3 + direction3 * m_CheckGround_Distance, Color.red, durationTime); Debug.DrawLine(rightStart3, rightStart3 + direction3 * m_CheckGround_Distance, Color.red, durationTime); } #endif } } /// /// @brief 水平移动 /// private void HorizontalMove() { // 获取水平输入 [-1, 1],松开按键时为 0 float horizontalValue = Input.GetAxis("Horizontal"); m_Rb.velocity = new Vector2(horizontalValue * m_HorizontalSpeedFactorPerSecond * Time.fixedDeltaTime, m_Rb.velocity.y); // Player 转向 if (horizontalValue < 0.0f) // 默认是右边 { m_BTurnDirection = true; transform.rotation = new Quaternion(0.0f, 180.0f, 0.0f, 1.0f); } else if (horizontalValue > 0.0f) { m_BTurnDirection = false; transform.rotation = new Quaternion(0.0f, 0.0f, 0.0f, 1.0f); } } /// /// @brief 跳跃 /// private void Jump() { if (m_BOnGround && m_BDoJump) { m_BDoJump = false; m_Rb.velocity = new Vector2(m_Rb.velocity.x, m_JumpSpeedPerSecond * Time.fixedDeltaTime); } if (!m_BOnGround && m_CurrentAllowAirJumpCount > 0 && m_BDoJump) { // 空中跳跃 m_BDoJump = false; --m_CurrentAllowAirJumpCount; m_Rb.velocity = new Vector2(m_Rb.velocity.x, m_JumpSpeedPerSecond * Time.fixedDeltaTime); } } /// /// @brief 下蹲 /// private void Crouch() { if (m_BPressedCrouch) { m_CapsuleCollider2D.size = new Vector2(m_CapsuleCollider2D.size.x, 0.6f); m_CapsuleCollider2D.offset = new Vector2(m_CapsuleCollider2D.offset.x, -0.545f); } else if (!m_BTopHasWall) // 上方如果检测到有墙不能起来 { m_CapsuleCollider2D.size = m_CapsuleCollider2DSize; m_CapsuleCollider2D.offset = m_CapsuleCollider2DOffset; } } } D. 瓦片裂缝问题?

✨参考链接

这里使用调节运行时分辨率的方式解决,之后可以优化

E. 小节演示

[入门卷] 5. 相机跟随 Player 移动(简单代码实现)

回到顶部

可以使用插件或代码或者两者结合使用,这里简单点使用纯代码实现,之后可能会单独出模块介绍

A. 实现简单相机跟随

给 Hierarchy 窗口中的 Main Camera 添加 脚本,命名为 CameraController

示例代码如下

using System.Collections; using System.Collections.Generic; using UnityEngine; public class CameraController : MonoBehaviour { public GameObject m_Player; void Start() { m_Player = GameObject.Find("Player"); } void Update() { transform.position = new Vector3(m_Player.transform.position.x, m_Player.transform.position.y, transform.position.z); } }

注意 Z轴坐标的调节 transform.position.z

附加:也可以用 Unity 中的插件 Cinemachine 实现,这里入门篇不做介绍

B. 小结演示

[入门篇] 6. 游戏场景切换

回到顶部

新建场景。Ctrl + N 或者在 Project xxx文件夹下 右键 Create -> Scene 或者 File -> New Scene -> Basic 2D (Build-in)

保存在 _Game/Scenes 文件夹下,这里命名为 Scene2

在场景中添加一个触发物品,这里用 Sunnyland 中自带的 Prop -> door 门。

右键 UI -> Panel,在 Panel 上,右键 UI -> Legacy -> Text。

这里我将画板 Panel 改了名字,Panel_EnterDoor

这里点击 EnterDoor,在 Animation 窗口点击 Create 可以创建空动画,这里点击录制按钮进行录制,比较简单,这里不做详细介绍。Text 文本可以按喜好输入文字。

这里用到了 “预制体”,在 .../_Game/Prefabs 文件夹,例如,将 Player 拖入到此文件夹中,或者在 文件夹中 右键 Create -> Prefab,这里使用前者。更改文件夹中的预制体可以更改所有预制体实例,或者通过 Hierarchy 窗口中的 预制体实例右边的 小箭头进入也可以修改全部,但是仅仅修改 预制体实例,是只影响单个的。这里只是简单介绍 Prefab 的使用。

第二个场景如下

同样要 Tilemap,更之前创建方法一样,这里不过多介绍,相机脚本要重新赋值一下

有个问题说一下,还是网格裂缝问题,这里最好改一下网格大小,改成 0.99,改分辨率没啥用

先切换到第一个场景,这里可以改个名字,MainScene

A. 代码部分 给 EnterDoor 添加脚本组件,命名为 DoorController,实例代码如下 using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.SceneManagement; public class DoorController : MonoBehaviour { [SerializeField] private bool m_BAtDoorPosition = false; [Header("UI 参数")] public GameObject m_UIEnterDoor; private void Start() { m_UIEnterDoor.SetActive(false); } private void Update() { if (m_BAtDoorPosition && Input.GetKeyDown(KeyCode.E)) { // using UnityEngine.SceneManagement // 加载当前激活场景的下一个场景 SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex + 1); } } /// /// @biref 进入碰撞体开关 /// private void OnTriggerEnter2D(Collider2D collision) { if (collision && collision.tag == "Player") { m_BAtDoorPosition = true; m_UIEnterDoor.SetActive(true); } } /// /// @biref 退出碰撞体开关 /// /// private void OnTriggerExit2D(Collider2D collision) { if (collision && collision.tag == "Player") { m_BAtDoorPosition = true; m_UIEnterDoor.SetActive(false); } } }

这里用到了 Inspector 中的 Tag,将 Player 的 Tag 设置为 Player,EnterDoor 可以设置默认禁用(我们在代码中也设置了,不影响)

组件启用是 enable,GameObject 激活是 SetActive。

这里还是说一下更改预制体的操作

将脚本挂到 door 上

B. 美化文字和画布

双击 Text 文字编辑

双击 Panel_EnterDoor,即可进入编辑模式

这里还可以做动画,例如增加渐入渐出的效果,使用录制功能,这里暂时不做介绍,我想放在单独的模块介绍

C. Build Settings 设置场景顺序

在菜单栏 File -> Build Settings

代码中我们用到的是场景的序号,然后这里需要设置一下场景

直接拖拽即可,或者点击添加打开的场景 Add Open Scenes

Ok,基本完成了,现在你可以运行你的项目

D. 小节演示

二、杂项篇

回到顶部

[杂项篇] 1. Visual Studio Code 配置 Unity

回到顶部

安装 C#,Uniy Code Snippets 2个插即可件

C# 支持 C#;

Uniy Code Snippets 提供 Awake(),Update() 等方法提示;

在 Unity 编辑器中 Edit -> Preferences -> External Tools -> External Script Editor -> 找到 Visual Studio Code,没有的话可以手动 Brower 到安装目录下找到 Code.exe;

我这里有下载 Visual Studio 2019,有安装 “使用 Unity 的游戏开发” 组件,如果没有,需要单独安装开发包,可以去 Vscode 官网看看怎么配置 Unity,简单一点就装一下 VS Unity 组件。(装了还没有提示,在 Vscode 设置中 开一下 Omnisharp Auto Start 试一下)

[杂项篇] 2. 瓦片裂缝问题?

回到顶部

有两种简单的方法这里先介绍一下,可以调节运行时的分辨率,或者改变网格大小,例如默认是 1,1,1 可以改成 0.99, 0.99, 0.99

更新补充 2022/12/12

人物转向时可能有 Bug,所以后面改用下面这种

/// /// @brief 水平移动 /// private void HorizontalMove() { // 获取水平输入 [-1, 1],松开按键时为 0 float horizontalValue = Input.GetAxis("Horizontal"); m_Rb.velocity = new Vector2(horizontalValue * m_HorizontalSpeedFactorPerSecond * Time.fixedDeltaTime, m_Rb.velocity.y); // Player 转向 if (horizontalValue < 0.0f) // 默认是右边 { m_BTurnDirection = true; transform.rotation = Quaternion.Euler(0.0f, 180.0f, 0.0f); } else if (horizontalValue > 0.0f) { m_BTurnDirection = false; transform.rotation = Quaternion.Euler(0.0f, 0.0f, 0.0f); } } 三、最后

第一次写这么长的篇幅,不知道大家习不习惯,可以在评论区评论或者给我留言

入门卷还未完结,之后还会介绍敌人、光照等等

那么,本卷的内容就到这里了,下卷会继续分享 Unity2D 游戏开发入门相关知识

The End.



【本文地址】

公司简介

联系我们

今日新闻


点击排行

实验室常用的仪器、试剂和
说到实验室常用到的东西,主要就分为仪器、试剂和耗
不用再找了,全球10大实验
01、赛默飞世尔科技(热电)Thermo Fisher Scientif
三代水柜的量产巅峰T-72坦
作者:寞寒最近,西边闹腾挺大,本来小寞以为忙完这
通风柜跟实验室通风系统有
说到通风柜跟实验室通风,不少人都纠结二者到底是不
集消毒杀菌、烘干收纳为一
厨房是家里细菌较多的地方,潮湿的环境、没有完全密
实验室设备之全钢实验台如
全钢实验台是实验室家具中较为重要的家具之一,很多

推荐新闻


图片新闻

实验室药品柜的特性有哪些
实验室药品柜是实验室家具的重要组成部分之一,主要
小学科学实验中有哪些教学
计算机 计算器 一般 打孔器 打气筒 仪器车 显微镜
实验室各种仪器原理动图讲
1.紫外分光光谱UV分析原理:吸收紫外光能量,引起分
高中化学常见仪器及实验装
1、可加热仪器:2、计量仪器:(1)仪器A的名称:量
微生物操作主要设备和器具
今天盘点一下微生物操作主要设备和器具,别嫌我啰嗦
浅谈通风柜使用基本常识
 众所周知,通风柜功能中最主要的就是排气功能。在

专题文章

    CopyRight 2018-2019 实验室设备网 版权所有 win10的实时保护怎么永久关闭